package net.xdclass.service.impl;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import net.xdclass.component.ShortLinkComponent;
import net.xdclass.config.RabbitMQConfig;
import net.xdclass.constant.RedisKey;
import net.xdclass.controller.request.*;
import net.xdclass.enums.BizCodeEnum;
import net.xdclass.enums.DomainTypeEnum;
import net.xdclass.enums.EventMessageType;
import net.xdclass.enums.ShortLinkStateEnum;
import net.xdclass.feign.TrafficFeignService;
import net.xdclass.interceptor.LoginInterceptor;
import net.xdclass.manager.DomainManager;
import net.xdclass.manager.GroupCodeMappingManager;
import net.xdclass.manager.LinkGroupManager;
import net.xdclass.manager.ShortLinkManager;
import net.xdclass.model.*;
import net.xdclass.mapper.ShortLinkMapper;
import net.xdclass.service.ShortLinkService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import net.xdclass.util.*;
import net.xdclass.vo.ShortLinkVO;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 王展
 * @since 2024-07-21
 */
@Service
@Slf4j
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLinkDO> implements ShortLinkService {
    @Autowired
    private ShortLinkManager shortLinkManager;
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private RabbitMQConfig rabbitMQConfig;
    @Autowired
    private DomainManager domainManager;
    @Autowired
    private LinkGroupManager linkGroupManager;
    @Autowired
    private ShortLinkComponent shortLinkComponent;
    @Autowired
    private GroupCodeMappingManager groupCodeMappingManager;
    @Autowired
    private RedisTemplate<Object,Object> redisTemplate;
    @Autowired
    private TrafficFeignService trafficFeignService;
    @Override
    public ShortLinkVO parseShortLinkCode(String shortLinkCode) {
        ShortLinkDO code = shortLinkManager.findByShortLinkCode(shortLinkCode);
        ShortLinkVO shortLinkVO = new ShortLinkVO();
        BeanUtils.copyProperties(code,shortLinkVO);
        return shortLinkVO;
    }

    @Override
    public JsonData createShortLink(ShortLinkAddRequest request) {
        Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
        //需要预先检查下是否有⾜够多的可以进⾏创建
        String cacheKey = String.format(RedisKey.DAY_TOTAL_TRAFFIC,accountNo);

        //检查key是否存在，然后递减，是否⼤于等于0，使⽤lua脚本
        // 如果key不存在，则未使⽤过，lua返回值是0； 新增流量包的时候，不⽤重新计算次数，直接删除key,消费的时候回计算更新
        String script = "if redis.call('get',KEYS[1]) then return redis.call('decr',KEYS[1]) else return 0 end";
        Long leftTimes = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Arrays.asList(cacheKey), "");
        log.info("今⽇流量包剩余次数:{}", leftTimes);

        if(leftTimes >= 0){
            String originalUrl = CommonUtil.addUrlPrefix(request.getOriginalUrl());
            request.setOriginalUrl(originalUrl);
            EventMessage eventMessage = EventMessage.builder().accountNo(accountNo)
                    .content(JsonUtil.obj2Json(request))
                    .messageId(IDUtil.geneSnowFlakeID().toString())
                    .eventMessageType(EventMessageType.SHORT_LINK_ADD.name())
                    .build();
            rabbitTemplate.convertAndSend(rabbitMQConfig.getShortLinkEventExchange(),rabbitMQConfig.getShortLinkAddRoutingKey(),eventMessage);
            return JsonData.buildSuccess();
        }else {
            //流量包不足
            return JsonData.buildResult(BizCodeEnum.TRAFFIC_REDUCE_FAIL);
        }
    }

    /**
     * 处理新增短链消息
     * @param eventMessage
     * @return
     */
    @Override
    public boolean handlerAddShortLink(EventMessage eventMessage) {
        Long accountNo = eventMessage.getAccountNo();
        String eventMessageType = eventMessage.getEventMessageType();
        String content = eventMessage.getContent();
        ShortLinkAddRequest addRequest = JsonUtil.json2Obj(content, ShortLinkAddRequest.class);
        //短链码是否重复
        boolean duplicateCodeFlag = false;
        //判断短链域名是否合法
        DomainDO domainDO = checkDomain(addRequest.getDomainType(), addRequest.getDomainId(), accountNo);
        //判断组名是否合法
        LinkGroupDO linkGroupDO = checkLinkGroup(addRequest.getGroupId(), accountNo);
        //⽣成⻓链摘要
        String originUrlDigest = CommonUtil.MD5(addRequest.getOriginalUrl());
        //⽣成短链码
        String shortLinkCode = shortLinkComponent.createShortLinkCode(addRequest.getOriginalUrl());
        //TODO 加锁
        //key1是短链码，ARGV[1]是accountNo,ARGV[2]是过期时间
        String script =
                //如果key1不存在
                "if redis.call('EXISTS',KEYS[1])==0 then redis.call('set',KEYS[1],ARGV[1]); redis.call('expire',KEYS[1],ARGV[2]); return 1;" +
                        //如果key1=accountno，代表可重入
                        " elseif redis.call('get',KEYS[1]) == ARGV[1] then return 2;" +
                        " else return 0; end;";
        //result>0加锁成功
        Long result = redisTemplate.execute(new DefaultRedisScript<>(script, Long.class), Collections.singletonList(shortLinkCode), accountNo, 100);
            if(result > 0) {
                //构建短链对象
                //C端处理
                if (EventMessageType.SHORT_LINK_ADD_LINK.name().equalsIgnoreCase(eventMessageType)) {
                    //查询短链码是否存在
                    ShortLinkDO shortLinkDO = shortLinkManager.findByShortLinkCode(shortLinkCode);
                    if(shortLinkDO == null){
                        //扣件流量包
                        boolean reduceFlag = reduceTraffic(eventMessage,shortLinkCode);
                        if(reduceFlag){
                            shortLinkDO = ShortLinkDO.builder().accountNo(accountNo)
                                    .code(shortLinkCode).title(addRequest.getTitle()).originalUrl(addRequest.getOriginalUrl())
                                    .domain(domainDO.getValue()).groupId(linkGroupDO.getId()).expired(addRequest.getExpired())
                                    .sign(originUrlDigest).state(ShortLinkStateEnum.ACTIVE.name()).del(0).build();
                            shortLinkManager.addShortLink(shortLinkDO);
                            return true;
                        }
                        return false;
                    }else{
                        log.error("C端短链码重复:{}",eventMessage);
                        duplicateCodeFlag = true;
                    }


                }
                //B端用户
                else if (EventMessageType.SHORT_LINK_ADD_MAPPING.name().equalsIgnoreCase(eventMessageType)) {
                    //为什么要传groupId和accountId是为了分库分表
                    GroupCodeMappingDO groupCodeMappingDO = groupCodeMappingManager.findByCodeAndGroupId(shortLinkCode, addRequest.getGroupId(), accountNo);
                    if(groupCodeMappingDO == null){
                        groupCodeMappingDO = GroupCodeMappingDO.builder().accountNo(accountNo)
                                .code(shortLinkCode).title(addRequest.getTitle()).originalUrl(addRequest.getOriginalUrl())
                                .domain(domainDO.getValue()).groupId(linkGroupDO.getId()).expired(addRequest.getExpired())
                                .sign(originUrlDigest).state(ShortLinkStateEnum.ACTIVE.name()).del(0).build();
                        groupCodeMappingManager.add(groupCodeMappingDO);
                        return true;
                    }else{
                        log.error("B端短链码重复:{}",eventMessage);
                        duplicateCodeFlag = true;
                    }


                }
            }else{
                //加锁失败，自旋100毫秒，再调用；失败的可能是短链码已经被占用，需要重新生成
                log.error("加锁失败",eventMessage);
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                }
                duplicateCodeFlag = true;
            }


            if(duplicateCodeFlag){
                String newOriginalUrl = CommonUtil.addUrlPrefixVersion(addRequest.getOriginalUrl());
                addRequest.setOriginalUrl(newOriginalUrl);
                eventMessage.setContent(JsonUtil.obj2Json(addRequest));
                log.warn("短链码报错失败，重新生成",eventMessage);
                handlerAddShortLink(eventMessage);
            }


        return false;

    }

    private boolean reduceTraffic(EventMessage eventMessage, String shortLinkCode) {
        //处理流量包扣减
        //根据短链类型，检查是否有⾜够多的流量包包 分布式事务问题
        UseTrafficRequest request = UseTrafficRequest.builder()
                        .accountNo(eventMessage.getAccountNo())
                        .bizId(shortLinkCode)
                        .build();
        JsonData jsonData = trafficFeignService.useTraffic(request);
        //使⽤流量包
        if (jsonData.getCode() != 0) {
            log.error("流量包不⾜，扣减失败:{}", eventMessage);
            return false;
        }
        return true;
    }

    @Override
    public Map pageByGroupId(ShortLinkPageRequest request) {
        LoginUser loginUser = LoginInterceptor.threadLocal.get();
        Map<String, Object> map = groupCodeMappingManager.pageShortLinkByGroupId(request.getPage(), request.getSize(), loginUser.getAccountNo(), request.getGroupId());
        return map;
    }

    @Override
    public JsonData del(ShortLinkDelRequest request) {
        Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
        EventMessage eventMessage = EventMessage.builder().accountNo(accountNo)
                .content(JsonUtil.obj2Json(request))
                .messageId(IDUtil.geneSnowFlakeID().toString())
                .eventMessageType(EventMessageType.SHORT_LINK_DEL.name())
                .build();
        rabbitTemplate.convertAndSend(rabbitMQConfig.getShortLinkEventExchange(),rabbitMQConfig.getShortLinkDelRoutingKey(),eventMessage);
        return JsonData.buildSuccess();
    }

    @Override
    public JsonData update(ShortLinkUpdateRequest request) {
        Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
        EventMessage eventMessage = EventMessage.builder().accountNo(accountNo)
                .content(JsonUtil.obj2Json(request))
                .messageId(IDUtil.geneSnowFlakeID().toString())
                .eventMessageType(EventMessageType.SHORT_LINK_UPDATE.name())
                .build();
        //TODO
        rabbitTemplate.convertAndSend(rabbitMQConfig.getShortLinkEventExchange(),rabbitMQConfig.getShortLinkUpdateRoutingKey(),eventMessage);

        return JsonData.buildSuccess();
    }

    @Override
    public boolean handlerDelShortLink(EventMessage eventMessage) {
        //Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
        Long accountNo = eventMessage.getAccountNo();
        ShortLinkDelRequest request = JsonUtil.json2Obj(eventMessage.getContent(), ShortLinkDelRequest.class);
        String code = request.getCode();
        Long groupId = request.getGroupId();
        Long mappingId = request.getMappingId();
        if(eventMessage.getEventMessageType().equalsIgnoreCase(EventMessageType.SHORT_LINK_DEL_LINK.name())){
            //C端
            int rows = shortLinkManager.del(code,accountNo);
            log.debug("删除C端短链,rows:{}",rows);
            return true;

        }else if(eventMessage.getEventMessageType().equalsIgnoreCase(EventMessageType.SHORT_LINK_DEL_MAPPING.name())){
            //B端
            int rows = groupCodeMappingManager.del(code, accountNo, groupId);
            log.debug("删除B端短链,rows:{}",rows);
            return true;
        }
        return false;
    }

    @Override
    public boolean handlerUpdateShortLink(EventMessage eventMessage) {
        //Long accountNo = LoginInterceptor.threadLocal.get().getAccountNo();
        Long accountNo = eventMessage.getAccountNo();
        ShortLinkUpdateRequest request = JsonUtil.json2Obj(eventMessage.getContent(), ShortLinkUpdateRequest.class);
        String code = request.getCode();
        Long domainId = request.getDomainId();
        String domainType = request.getDomainType();
        String title = request.getTitle();
        Long groupId = request.getGroupId();
        DomainDO domainDO = checkDomain(domainType, domainId, accountNo);
        if(eventMessage.getEventMessageType().equalsIgnoreCase(EventMessageType.SHORT_LINK_UPDATE_LINK.name())){
            //C端
            ShortLinkDO shortLinkDO = ShortLinkDO.builder().code(code).domain(domainDO.getValue()).title(title).groupId(groupId).accountNo(accountNo).build();
            int rows = shortLinkManager.update(shortLinkDO);
            log.debug("更新C端短链,rows:{}",rows);
            return true;

        }else if(eventMessage.getEventMessageType().equalsIgnoreCase(EventMessageType.SHORT_LINK_UPDATE_MAPPING.name())){
            //B端
            GroupCodeMappingDO groupCodeMappingDO = GroupCodeMappingDO.builder().id(request.getMappingId()).code(code).domain(domainDO.getValue()).title(title).groupId(groupId).accountNo(accountNo).build();
            int rows = groupCodeMappingManager.update(groupCodeMappingDO);
            log.debug("更新B端短链,rows:{}",rows);
            return true;
        }
        return false;
    }

    /**
     * 校验域名
     * @param domainType
     * @param domainId
     * @param accountNo
     * @return
     */
    private DomainDO checkDomain(String domainType,Long domainId,Long accountNo){
        DomainDO domainDO;
        //自定义域名
        if(DomainTypeEnum.CUSTOM.name().equalsIgnoreCase(domainType)){
            domainDO = domainManager.findById(domainId,accountNo);
        }else{
            //官方域名
            domainDO = domainManager.findByDomainTypeAndID(domainId,DomainTypeEnum.OFFICIAL);
        }
        //传进来两个参数，判断object是否为空，是的话就抛出IllegalArgumentException异常，且异常信息是传进来message的内容。
        Assert.notNull(domainDO,"短链域名不合法");
        return domainDO;
    }

    /**
     * 校验组名
     * @param groupId
     * @param accountNo
     * @return
     */
    private LinkGroupDO checkLinkGroup(Long groupId,Long accountNo){
        LinkGroupDO linkGroupDO = linkGroupManager.detail(groupId, accountNo);
        Assert.notNull(linkGroupDO,"组名不合法");
        return linkGroupDO;

    }
}
